Skip to content

Preserve a transparent base when pasting with alpha on the GD driver - #881

Open
nlemoine wants to merge 1 commit into
php-imagine:developfrom
nlemoine:fix/gd-paste-alpha-transparent-base
Open

nlemoine wants to merge 1 commit into
php-imagine:developfrom
nlemoine:fix/gd-paste-alpha-transparent-base

Conversation

@nlemoine

@nlemoine nlemoine commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

GD counterpart of #878, which fixed the same thing for Imagick. Found it while pasting a watermark over a transparent PNG.

Problem: Gd\Image::paste($image, $start, $alpha) sends 0 < alpha < 100 through imagecopymerge(). That function only reads the source RGB (it ignores the source per-pixel alpha), and every destination pixel it touches ends up opaque. So when the base has transparency, the area under the overlay is forced opaque and its color gets pulled towards whatever RGB the transparent base stored, which is white for a normal PNG. You end up with an opaque near-white block where the paste happened. An opaque base (a JPEG) is fine, it only breaks when the base itself has transparency.

Minimal repro:

use Imagine\Gd\Imagine;
use Imagine\Image\Box;
use Imagine\Image\Point;
use Imagine\Image\Palette\RGB;

$imagine = new Imagine();
$palette = new RGB();

$base = $imagine->create(new Box(20, 20), $palette->color([255, 255, 255], 0)); // transparent
$overlay = $imagine->create(new Box(10, 10), $palette->color([0, 128, 0]));      // opaque green
$base->paste($overlay, new Point(0, 0), 40);

$base->getColorAt(new Point(5, 5));
// before: #99cc99, alpha 100  (opaque, washed towards white)
// after:  #008000, alpha 40   (overlay color at 40%, base stays semi-transparent)

Fix: fade the overlay's own alpha channel by the opacity factor, then paste at full opacity so it goes through the alpha-aware imagecopy() path instead of imagecopymerge(). I clone the overlay first so the caller's image is left untouched. Same approach as #878 did for Imagick (evaluateImage(MULTIPLY, factor, CHANNEL_ALPHA) then composite), and as Intervention/image#1489 did for GD. GD has no native equivalent of Imagick's evaluateImage() on the alpha channel, so the overlay alpha is scaled pixel by pixel.

Opaque bases behave exactly as before, testPasteWithAlpha and testPasteWithAlphaPreservesTransparency still pass with the same values.

Testing: I added testPasteWithAlphaPreservesTransparentBase next to the existing opaque-base test, in AbstractImageTest so every driver runs it. It fails on GD before the change, passes on Imagick, and is skipped on Gmagick (no alpha paste there). Full suite is green in the 8.1-gd-imagick docker image (GD bundled 2.1 compatible), 783 tests, and I also ran it against a newer GD (2.3.3) locally.

Gd\Image::paste() routed 0 < alpha < 100 through imagecopymerge(),
which ignores the source's per-pixel alpha and resolves every touched
destination pixel as fully opaque. Pasting an overlay over a base that
has transparency therefore washed the transparent regions into an
opaque block (near-white for a typical PNG whose transparent pixels
are stored white).

Fade the overlay's own alpha channel by the opacity factor and paste
it at full opacity through the alpha-aware imagecopy() path instead,
so the base's transparency survives. Mirrors the Imagick change in
php-imagine#878.

Add testPasteWithAlphaPreservesTransparentBase covering a transparent
destination, companion to the existing opaque-base test.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant